﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace XNOgre.Core
{
    public class Node : IDisposable
    {
        public enum TransformSpace
        {
            Local,
            Parent,
            World
        }

        public event EventHandler NodeUpdated;
        public event EventHandler NodeDestroyed;
        public event EventHandler NodeAttached;
        public event EventHandler NodeDetatched;

        private Node parent;
        protected List<Node> children;
        private List<Node> childrenToUpdate;
        private bool needParentUpdate;
        private bool needChildUpdate;
        private bool parentNotified;
        private bool queuedForUpdate;
        protected string name;

        protected Quaternion orientation, derivedOrientation;
        protected Vector3 position, derivedPosition;
        protected Vector3 scale, derivedScale;
        private bool inheritScale, inheritOrientation;

        private Matrix cachedTransform;
        private bool cachedTransformOutOfDate;

        private List<Node> queuedUpdate;
        private object tag;

        public Node()
        {
            parent = null;
            needParentUpdate = false;
            needChildUpdate = false;
            parentNotified = false;
            queuedForUpdate = false;
            orientation = Quaternion.Identity;
            position = Vector3.Zero;
            scale = Vector3.One;
            inheritOrientation = true;
            inheritScale = true;
            derivedOrientation = Quaternion.Identity;
            derivedPosition = Vector3.Zero;
            derivedScale = Vector3.One;
            cachedTransformOutOfDate = true;

            childrenToUpdate = new List<Node>();
            //Generate a name
            this.name = Root.GenerateName(this.GetType());

            children = new List<Node>();
            NeedUpdate();
        }
        internal Node(string name)
        {
            childrenToUpdate = new List<Node>();
            children = new List<Node>();

            this.parent = null;
            this.needParentUpdate = false;
            this.needChildUpdate = false;
            this.parentNotified = false;
            this.queuedForUpdate = false;
            this.name = name;
            this.orientation = Quaternion.Identity;
            this.position = Vector3.Zero;
            this.scale = Vector3.One;
            this.inheritOrientation = true;
            this.inheritScale = true;
            this.derivedOrientation = Quaternion.Identity;
            this.derivedPosition = Vector3.Zero;
            this.derivedScale = Vector3.One;
            this.cachedTransformOutOfDate = true;
            NeedUpdate();
        }
        public virtual void Dispose()
        {
            if (NodeDestroyed != null)
            {
                NodeDestroyed(this, EventArgs.Empty);
            }

            RemoveAllChildren();

            if (parent != null)
                parent.RemoveChild(this);

            if (queuedForUpdate)
            {
                int indexToRemove = -1;
                for (int i = 0; i < queuedUpdate.Count; i++)
                {
                    if (queuedUpdate[i] == this)
                    {
                        indexToRemove = i;
                        break;
                    }
                }
                if (indexToRemove != -1)
                {
                    queuedUpdate.RemoveAt(indexToRemove);
                }
            }
        }
        public string Name
        {
            get { return name; }
            protected set { name = value; }
        }
        /// <summary>
        /// Gets this node's parent (null if this is the root).
        /// </summary>
        public virtual Node Parent
        {
            get { return parent; }
            set
            {
                bool different = (parent != value);

                parent = value;
                parentNotified = false;
                NeedUpdate();

                if (different)
                {
                    if (parent != null)
                    {
                        if (NodeAttached != null)
                        {
                            NodeAttached(this, EventArgs.Empty);
                        }
                    }
                    else
                    {
                        if (NodeDetatched != null)
                        {
                            NodeAttached(this, EventArgs.Empty);
                        }
                    }
                }
            }
        }
        /// <summary>
        /// Gets/Sets orientation of this node via a quaternion.
        /// </summary>
        public virtual Quaternion Orientation
        {
            get { return orientation; }
            set
            {
                orientation = Quaternion.Normalize(value);
                NeedUpdate();
            }
        }
        public virtual Vector3 Position
        {
            get { return position; }
            set
            {
                position = value;
                NeedUpdate();
            }
        }

        public Vector3 Scale
        {
            get { return scale; }
            set
            {
                scale = value;
                NeedUpdate();
            }
        }

        /// <summary>
        /// Get/Sets if this node is affected by orientation applied to the parent nod.
        /// </summary>
        public bool InheritOrientation
        {
            get { return inheritOrientation; }
            set
            {
                inheritOrientation = value;
                NeedUpdate();
            }
        }

        /// <summary>
        /// Gets/Sets whether this node is affected by scaling factors applied to the parent node.
        /// </summary>
        public bool InheritScale
        {
            get { return inheritScale; }
            set
            {
                inheritScale = value;
                NeedUpdate();
            }
        }
        public int NumChildren
        {
            get { return children.Count; }
        }
        public Vector3 DerivedScale
        {
            get
            {
                if (needParentUpdate)
                {
                    UpdateFromParent();
                }
                return derivedScale;
            }
        }

        public Matrix FullTransform
        {
            get
            {
                if (cachedTransformOutOfDate)
                {
                    //use derived values
                    cachedTransform = Matrix.CreateFromQuaternion(DerivedOrientation) * Matrix.CreateScale(DerivedScale) * Matrix.CreateTranslation(DerivedPosition);
                    cachedTransformOutOfDate = false;
                }
                return cachedTransform;
            }
        }

        public Vector3 DerivedPosition
        {
            get
            {
                if (needParentUpdate)
                {
                    UpdateFromParent();
                }
                return derivedPosition;
            }
            set
            {
                Position = parent.ConvertWorldToLocalPosition(value);
            }
        }

        public Quaternion DerivedOrientation
        {
            get
            {
                if (needParentUpdate)
                {
                    UpdateFromParent();
                }
                return derivedOrientation;
            }
            set
            {
                Orientation = parent.ConvertWorldToLocalOrientation(value);
            }
        }
        /// <summary>
        /// Allows the user to associate any personal data with the Node.
        /// </summary>
        public object Tag
        {
            get { return tag; }
            set { tag = value; }
        }
        /// <summary>
        /// Combines the current scale with thew passed in one. This is different from setting <code>Scale</code> where setting does not take into account what it already was.
        /// </summary>
        /// <param name="scale"></param>
        public virtual void ScaleBy(Vector3 scale)
        {
            this.scale *= scale;
            NeedUpdate();
        }
        /// <summary>
        /// This method moves the node by the supplied vector along the world Cartesian axes.
        /// </summary>
        /// <param name="t"></param>
        /// <param name="relativeTo"></param>
        public virtual void Translate(Vector3 t, TransformSpace relativeTo = TransformSpace.Local)
        {
            if (t == Vector3.Zero)
                return;
            
            var qnorm = Quaternion.Normalize(orientation);

            switch (relativeTo)
            {
                case TransformSpace.Local:
                    Position += Vector3.Transform(t, qnorm);

                    break;
                case TransformSpace.Parent:
                    Position += t;
                    break;
                case TransformSpace.World:
                    if (parent != null)
                    {
                        Position += (Vector3.Transform(t, Quaternion.Inverse(parent.DerivedOrientation))) / parent.DerivedScale;
                    }
                    else
                    {
                        Position += t;
                    }
                    break;
            }
            NeedUpdate();
        }
        public virtual void Translate(Matrix axes, Vector3 move, TransformSpace relativeTo = TransformSpace.Parent)
        {
            Vector3 derived = Vector3.Transform(move, axes);
            Translate(derived, relativeTo);
        }
        /// <summary>
        /// Rotate the node around the Z-Axis
        /// </summary>
        /// <param name="angle">angle, In Radians</param>
        /// <param name="relativeTo"></param>
        public virtual void Roll(float angle, TransformSpace relativeTo = TransformSpace.Local)
        {
            Rotate(Vector3.Forward, angle, relativeTo);
        }
        /// <summary>
        /// Rotate the node along the X-Axis
        /// </summary>
        /// <param name="angle">angle, In Radians</param>
        /// <param name="relativeTo"></param>
        public virtual void Pitch(float angle, TransformSpace relativeTo = TransformSpace.Local)
        {

            Rotate(Vector3.Right, angle, relativeTo);
        }
        /// <summary>
        /// Rotate the node along the Y-axis.
        /// </summary>
        /// <param name="angle"></param>
        /// <param name="relativeTo"></param>
        public virtual void Yaw(float angle, TransformSpace relativeTo = TransformSpace.Local)
        {
            //flip to ensure rotation around local axes
            if (relativeTo == TransformSpace.Local)
                relativeTo = TransformSpace.Parent;

            else if (relativeTo == TransformSpace.Parent)
                relativeTo = TransformSpace.Local;

            Rotate(Vector3.Up, angle, relativeTo);
        }
        /// <summary>
        /// Rotate the node around an arbitrary axis.
        /// </summary>
        /// <param name="axis">angle, In Radians</param>
        /// <param name="angle"></param>
        /// <param name="relativeTo"></param>
        public virtual void Rotate(Vector3 axis, float angle, TransformSpace relativeTo = TransformSpace.Local)
        {
            var q = Quaternion.CreateFromAxisAngle(axis, angle);
            Rotate(q, relativeTo);
        }
        /// <summary>
        /// Rotate the node around an arbitrary axis using a Quaternion.
        /// </summary>
        /// <param name="rotation"></param>
        /// <param name="relativeTo"></param>
        public virtual void Rotate(Quaternion rotation, TransformSpace relativeTo = TransformSpace.Local)
        {
            rotation = Quaternion.Normalize(rotation);
            
            switch (relativeTo)
            {
                case TransformSpace.Parent:
                    Orientation = (rotation * this.orientation);
                    break;
                case TransformSpace.World:
                    Orientation = orientation * Quaternion.Inverse(DerivedOrientation) * rotation * DerivedOrientation;
                    break;
                case TransformSpace.Local:
                    Orientation = (orientation * rotation);
                    break;
            }

            NeedUpdate();
        }
        /// <summary>
        /// Creates child using default parameters
        /// </summary>
        /// <returns></returns>
        public virtual Node CreateChild()
        {
            return CreateChild(Vector3.Zero, Quaternion.Identity);
        }
        public virtual Node CreateChild(Vector3 translate)
        {
            return CreateChild(translate, Quaternion.Identity);
        }
        /// <summary>
        /// Creates an unnamed new Node as a child of this node.
        /// </summary>
        /// <param name="translate">Initial tranlsation of child relative to parent</param>
        /// <param name="rotate">Initial rotation relative to parent</param>
        /// <returns></returns>
        public virtual Node CreateChild(Vector3 translate, Quaternion rotate)
        {
            Node newNode = CreateChildImpl();
            newNode.Translate(translate);
            newNode.Rotate(rotate);
            this.AddChild(newNode);

            return newNode;
        }
        /// <summary>
        /// Creates a new named Node as a child of this node.
        /// </summary>
        /// <param name="name">Name, which allows you to look up the node from the parent</param>
        /// <param name="translate"></param>
        /// <param name="rotate"></param>
        /// <returns></returns>
        public virtual Node CreateChild(string name, Vector3 translate, Quaternion rotate)
        {
            Node newNode = CreateChildImpl(name);
            newNode.Translate(translate);
            newNode.Rotate(rotate);
            this.AddChild(newNode);
            return newNode;
        }
        /// <summary>
        /// Drops the specified child from this node.
        /// </summary>
        /// <remarks>
        /// Does not delete the node, just detatches it from this parent, potentially to
        /// be reattached elsewhere. There is also an alternate version which drops a named child from this node.
        /// </remarks>
        /// <param name="index"></param>
        /// <returns></returns>
        public virtual Node RemoveChild(int index)
        {


            if (index < children.Count)
            {
                var child = children[index];

                CancelUpdate(child);
                children.Remove(child);
                child.SetParent(null);
                return child;
            }
            else
            {
                throw new IndexOutOfRangeException("Child index out of bounds.");
            }

        }
        /// <summary>
        /// Drops the specified child from this node.
        /// </summary>
        /// <remarks>
        /// Does not delete the node, just detatches it from this parent, potentially to
        /// be reattached elsewhere. There is also an alternate version which drops a named child from this node.
        /// </remarks>
        public virtual Node RemoveChild(string name)
        {
            for (int i = 0; i < children.Count; i++)
            {
                var theNode = children[i];
                if (theNode.name == name)
                {
                    CancelUpdate(theNode);
                    theNode.SetParent(null);
                    children.RemoveAt(i);
                    return theNode;
                }
            }

            throw new Exception("Child node named " + name + " does not exist.");

        }
        /// <summary>
        /// Drops the specified child from this node.
        /// </summary>
        /// <remarks>
        /// Does not delete the node, just detatches it from this parent, potentially to
        /// be reattached elsewhere. There is also an alternate version which drops a named child from this node.
        /// </remarks>
        public virtual void RemoveChild(Node child)
        {
            if (child != null)
            {
                for (int i = 0; i < children.Count; i++)
                {
                    var theChild = children[i];
                    if (theChild == child)
                    {
                        CancelUpdate(theChild);
                        theChild.SetParent(null);
                        children.RemoveAt(i);
                        break;
                    }
                }
            }
        }
        /// <summary>
        /// Adds a (precreated) child scene node to this node. If it is attached to another node, it must be detatched first.
        /// </summary>
        /// <param name="child">The node to become the child of this node.</param>
        public virtual void AddChild(Node child)
        {
            if (child.parent != null)
            {
                throw new InvalidOperationException("Node " + child.name + " already was a child of " + child.parent.name + ".");
            }
            children.Add(child);
            child.SetParent(this);
        }
        public Node GetChild(int index)
        {
            return children[index];
        }
        public Node GetChild(string name)
        {
            foreach (Node child in children)
            {
                if (child.name == name)
                    return child;
            }

            throw new InvalidOperationException("No child named " + name + " could be found.");
        }
        public virtual void RemoveAllChildren()
        {
            foreach (Node child in children)
            {
                child.SetParent(null);
            }
            children.Clear();
            childrenToUpdate.Clear();
        }

        public void ResetOrientation()
        {
            orientation = Quaternion.Identity;
            NeedUpdate();
        }
        /// <summary>
        /// Resets the position / orientation /scale of this node to it's initial state.
        /// </summary>
        public void ResetToInitialState()
        {
            position = Vector3.Zero;
            orientation = Quaternion.Identity;
            scale = Vector3.One;

            NeedUpdate();
        }
        protected virtual void SetParent(Node parent)
        {
            bool different = (this.parent != parent);

            this.parent = parent;
            this.parentNotified = false;
            NeedUpdate();

            if (different)
            {
                if (parent != null)
                {
                    if (NodeAttached != null)
                    {
                        NodeAttached(this, EventArgs.Empty);
                    }
                }
                else
                {
                    if (NodeDetatched != null)
                    {
                        NodeDetatched(this, EventArgs.Empty);
                    }
                }
            }
        }
        protected void UpdateFromParent()
        {
            needParentUpdate = false;

            UpdateFromParentImpl();

            if (NodeUpdated != null)
            {
                NodeUpdated(this, EventArgs.Empty);
            }
        }
        protected virtual void UpdateFromParentImpl()
        {
            if (parent != null)
            {
                //update orienation
                Quaternion parentOrientation = parent.DerivedOrientation;
                if (inheritOrientation)
                {
                    derivedOrientation = parentOrientation * orientation;
                }
                else
                {
                    derivedOrientation = orientation;
                }
                //update scale
                Vector3 parentScale = parent.DerivedScale;
                if (inheritScale)
                {
                    derivedScale = parentScale * scale;
                }
                else
                {
                    derivedScale = scale;
                }

                //Change position vector based on parent's orientation & scale
                derivedPosition = Vector3.Transform((parentScale * position), parentOrientation);

                //Add altered position vector to parents
                derivedPosition += parent.DerivedPosition;
            }
            else
            {
                //Root node, no parent
                derivedOrientation = orientation;
                derivedPosition = position;
                derivedScale = scale;
            }

            cachedTransformOutOfDate = true;
            needParentUpdate = false;
        }

        protected virtual Node CreateChildImpl() { throw new NotImplementedException(); }
        protected virtual Node CreateChildImpl(string name) { throw new NotImplementedException(); }

        public virtual void Update(bool updateChildren, bool parentHasChanged)
        {
            parentNotified = false;

            if (needParentUpdate || parentHasChanged)
            {
                UpdateFromParent();
            }

            if (updateChildren)
            {
                if (needChildUpdate || parentHasChanged)
                {
                    foreach (Node child in children)
                    {
                        child.Update(true, true);
                    }
                }
                else
                {
                    for (int i = 0; i < childrenToUpdate.Count; i++)
                    {
                        childrenToUpdate[i].Update(true, false);
                    }
                }
                childrenToUpdate.Clear();
                needChildUpdate = false;

            }
        }


        public virtual Vector3 ConvertWorldToLocalPosition(Vector3 world)
        {
            if (needParentUpdate)
                UpdateFromParent();

            return Vector3.Transform((world - derivedPosition) / derivedScale, Quaternion.Inverse(derivedOrientation));
        }
        public virtual Vector3 ConvertLocalToWorldPosition(Vector3 local)
        {
            if (needParentUpdate)
                UpdateFromParent();

            return Vector3.Transform((local * derivedScale) + derivedPosition, derivedOrientation);
        }
        public virtual Quaternion ConvertWorldToLocalOrientation(Quaternion world)
        {
            if (needParentUpdate)
                UpdateFromParent();

            return Quaternion.Inverse(derivedOrientation) * world;
        }
        public virtual Quaternion ConvertLocalToWorldOrientation(Quaternion local)
        {
            if (needParentUpdate)
                UpdateFromParent();

            return derivedOrientation * local;
        }
        public virtual float GetSquaredViewDepth(Camera cam)
        {
            Vector3 diff = DerivedPosition - cam.DerivedPosition;

            return diff.LengthSquared();
        }

        public virtual void NeedUpdate(bool forceParentUpdate = false)
        {
            needParentUpdate = true;
            needChildUpdate = true;
            cachedTransformOutOfDate = true;

            //Make sure we're not root and parent hasn't been notified before
            if (parent != null && (!parentNotified || forceParentUpdate))
            {
                parent.RequestUpdate(this, forceParentUpdate);
                parentNotified = true;
            }

            childrenToUpdate.Clear();
        }
        public virtual void RequestUpdate(Node child, bool forceParentUpdate = false)
        {
            // If we're already going to update everything this doesn't matter
            if (needChildUpdate)
            {
                return;
            }

            childrenToUpdate.Add(child);
            //Request selecteive update of me, if we didn't do it before
            if (parent != null && (!parentNotified || forceParentUpdate))
            {
                parent.RequestUpdate(this, forceParentUpdate);
                parentNotified = true;
            }
        }
        public virtual void CancelUpdate(Node child)
        {
            if (childrenToUpdate.Contains(child))
            {
                childrenToUpdate.Remove(child);
            }

            //Propagate this up if we're done
            if (childrenToUpdate.Count == 0 && parent != null && !needChildUpdate)
            {
                parent.CancelUpdate(this);
                parentNotified = false;
            }
        }

        static List<Node> queuedUpdates = new List<Node>();

        public static void QueueNeedUpdate(Node n)
        {
            //Don't queue the node more then once
            if (n.queuedForUpdate == false)
            {
                n.queuedForUpdate = true;
                queuedUpdates.Add(n);
            }
        }
        public static void ProcessQueuedUpdates()
        {
            for (int i = 0; i < queuedUpdates.Count; i++)
            {
                Node n = queuedUpdates[i];
                n.queuedForUpdate = false;
                n.NeedUpdate(true);
            }
            queuedUpdates.Clear();
        }

    }
}
